﻿using System;
using System.Collections.Generic;
using System.IO;
using Expect.Core.BaseClasses;
using Expect.Core.Fixtures;
using Expect.Core.Interfaces;

namespace Expect.Core.IO
{
    using System.Linq;

    /// <summary>
    /// Reader implementation for text format
    /// </summary>
    public class TextSpecificationReader : TextSpecification, ISpecificationReader
    {
        private INameProvider _nameprovider;

        /// <summary>
        /// Reader constructor
        /// </summary>
        public TextSpecificationReader()
        {
            _nameprovider = new DefaultNameProvider();
        }

        /// <summary>
        /// Reader constructor
        /// </summary>
        /// <param name="provider"></param>
        public TextSpecificationReader(INameProvider provider)
        {
            _nameprovider = provider;
        }

        /// <summary>
        /// Read a specification from the provided string
        /// </summary>
        /// <param name="inputname">name of the input, for example, file name without extension</param>
        /// <param name="s">the string representation of the specification</param>
        /// <returns>spec definition</returns>
        public ParsingResult ReadSpecification(string inputname, string s)
        {
            ParsingResult res = new ParsingResult();
            res.Specification = new SpecificationDefinition();
            using (StringReader reader = new StringReader(s))
            {
                res = ReadSpecNew(inputname, reader);
            }
            return res;
        }

        /// <summary>
        /// Read a specification from the provided stream
        /// </summary>
        /// <param name="inputname">name of the input, for example, file name without extension</param>
        /// <param name="stream">the stream providing the specification</param>
        /// <returns>spec definition</returns>
        public ParsingResult ReadSpecification(string inputname, Stream stream)
        {
            ParsingResult res = null;
            using (StreamReader reader = new StreamReader(stream))
            {
                res = ReadSpecNew(inputname, reader);
            }
            return res;
        }

        private const string BlocPrefix = "@";
        private const string ContainerBloc = "containerbase";
        private const string SetupBloc = "setup";
        private const string SetupFromBloc = "setupfrom";
        private const string SpecBloc = "specification";
        private const string TeardownBloc = "teardown";
        private const string TeardownFromBloc = "teardownfrom";

        private string[] blocidentifiers = new string[] { SetupBloc, SpecBloc, TeardownBloc, ContainerBloc, SetupFromBloc, TeardownFromBloc };
        private string[] singlelineblocidentifiers = new string[] { ContainerBloc, SetupFromBloc, TeardownFromBloc };


        private bool IsBlocIdentifier(string tmp)
        {
            if (!string.IsNullOrEmpty(this.GetBlocIdentifier(tmp))) return true;

            return false;
        }

        private bool IsSingleLineBlocIdentifier(string tmp)
        {
            var id = this.GetBlocIdentifier(tmp);
            if (string.IsNullOrEmpty(id)) return false;

            return singlelineblocidentifiers.Any(e => e == id);  
        }

        private string GetBlocIdentifier(string tmp)
        {
            foreach (var id in blocidentifiers)
            {
                if (tmp.StartsWith(BlocPrefix + id + " ", StringComparison.OrdinalIgnoreCase))
                {
                    return id;
                }
            }

            return null;
        }

        private ParsingResult ReadSpecNew(string inputname, TextReader reader)
        {
            ParsingResult res = new ParsingResult();

            Dictionary<string, List<ParsingMetaData>> blocs = new Dictionary<string, List<ParsingMetaData>>();

            ReadBlocs(inputname, reader, blocs);
            ProcessBlocks(res, blocs);

            return res;
        }

        private void ProcessBlocks(ParsingResult result, Dictionary<string, List<ParsingMetaData>> blocs)
        {
            SpecificationDefinition setup = null;
            SpecificationDefinition teardown = null;
            List<SpecificationDefinition> lstspecs = new List<SpecificationDefinition>();
            SpecificationDefinition res = null;
            //bool setupfrom = false;
            //bool teardownfrom = false;

            foreach (var bloc in blocs)
            {
                var id = this.GetBlocIdentifier(bloc.Key);
                var buffer = bloc.Value;
                string blocname = null;

                if (!string.IsNullOrEmpty(id))
                {
                    blocname = bloc.Key.Substring(id.Length + 1).Trim();
                    var elt = buffer.First(l => l.Content == bloc.Key);
                    if (elt != null)
                        buffer.Remove(elt);
                }

                /*
                if (id == SetupBloc && (setup != null || setupfrom))
                    throw new InvalidOperationException("You could define only one setup bloc");
                if (id == TeardownBloc && (teardown != null || teardownfrom))
                    throw new InvalidOperationException("You could define only one teardown bloc");
                */

                if (id == SetupBloc && setup != null)
                    throw new InvalidOperationException("You could define only one setup bloc");
                if (id == TeardownBloc && teardown != null)
                    throw new InvalidOperationException("You could define only one teardown bloc");

                if (id == ContainerBloc)
                    result.ContainerBaseClass = blocname;
                else if (id == SetupFromBloc)
                {
                    result.GetSetupFrom = blocname;
                    //setupfrom = true;
                }
                else if (id == TeardownFromBloc)
                {
                    result.GetTeardownFrom = blocname;
                    //teardownfrom = true;
                }
                else if (id == SetupBloc)
                    setup = this.ReadBloc(blocname, bloc.Value);
                else if (id == TeardownBloc)
                    teardown = this.ReadBloc(blocname, bloc.Value);
                else if (id == SpecBloc)
                    lstspecs.Add(this.ReadBloc(blocname, bloc.Value));
            }

            if (lstspecs.Count == 1)
            {
                res = lstspecs[0];
                res.Setup = setup;
                res.Teardown = teardown;
                result.Specification = res;
                return;
            }

            res = new SpecificationDefinition();
            res.Setup = setup;
            res.Teardown = teardown;

            foreach (var spec in lstspecs)
            {
                res.AddChildSpec(spec);
            }

            result.Specification = res;
        }

        private SpecificationDefinition ReadBloc(string blocname, List<ParsingMetaData> bloc)
        {
            SpecificationDefinition res = new SpecificationDefinition();
            if (!string.IsNullOrEmpty(blocname))
            {
                res.Name = blocname;
                res.ClassName = _nameprovider.ElementName(blocname);
            }

            //StringBuilder commentBuffer = new StringBuilder();
            FixtureDefinition currentFixture = null;

            foreach (var line in bloc)
            {
                if (string.IsNullOrEmpty(line.TrimmedContent))
                {
                    currentFixture = ProcessBlankLine(currentFixture, res);
                }
                else
                {
                    currentFixture = ProcessLine(res, line, currentFixture);
                }
            }

            if (currentFixture != null)
            {
                ProcessBlankLine(currentFixture, res);
            }

            return res;
        }

        private void ReadBlocs(string inputname, TextReader reader, Dictionary<string, List<ParsingMetaData>> blocs)
        {
            string line = null;
            string id = null;
            List<ParsingMetaData> buffer = new List<ParsingMetaData>();
            int lineNumber = 0;
            while ((line = reader.ReadLine()) != null)
            {
                lineNumber++;
                var trimmedLine = line.Trim();
                
                if (this.IsSingleLineBlocIdentifier(trimmedLine))
                {
                    var elt = new List<ParsingMetaData>();
                    elt.Add(new ParsingMetaData{Content = line, TrimmedContent = trimmedLine, LineNumber = lineNumber});
                    blocs.Add(trimmedLine, elt);
                    continue;
                }
                if (IsBlocIdentifier(trimmedLine))
                {
                    if (buffer.Count > 0 && !string.IsNullOrEmpty(id))
                    {
                        if (blocs.ContainsKey(id))
                            throw new InvalidOperationException("The element already exists : " + trimmedLine);
                        blocs.Add(id, buffer);
                        buffer = new List<ParsingMetaData>();
                    }
                    id = trimmedLine;
                }

                buffer.Add(new ParsingMetaData { LineNumber = lineNumber, Content = line, TrimmedContent = trimmedLine });
            }

            if (buffer.Count > 0)
            {
                if (string.IsNullOrEmpty(id))
                {
                    id = BlocPrefix + SpecBloc + " " + Path.GetFileNameWithoutExtension(inputname);
                    buffer.Insert(0, new ParsingMetaData { LineNumber = -1, Content = id });
                }
                blocs.Add(id, buffer);
                buffer = new List<ParsingMetaData>();
            }
        }
        /*
        private SpecificationDefinition ReadSpec(TextReader reader)
        {
            SpecificationDefinition res = null;

            StringBuilder commentBuffer = new StringBuilder();
            res = new SpecificationDefinition();
            FixtureDefinition currentFixture = null;
            string tmp = null;
            while ((tmp = reader.ReadLine()) != null)
            {
                tmp = tmp.Trim();
                if (string.IsNullOrEmpty(tmp))
                {
                    currentFixture = ProcessBlankLine(currentFixture, ref commentBuffer, res);
                }
                else
                {
                    if (tmp.StartsWith(CommentToken))
                    {
                        ProcessCommentLine(tmp, commentBuffer);
                    }
                    else
                    {
                        currentFixture = ProcessLine(res, tmp, currentFixture, ref commentBuffer);
                    }
                }
            }

            if (currentFixture != null)
            {
                ProcessBlankLine(currentFixture, ref commentBuffer, res);
            }
            reader.Close();

            return res;
        }*/

        private FixtureDefinition ProcessLine(SpecificationDefinition parent, ParsingMetaData line, FixtureDefinition currentFixture)
        {
            bool isDescriptionLine = line.TrimmedContent.StartsWith(CommentToken);
            bool isImageLine = line.TrimmedContent.StartsWith(CommentToken + "img ");
            bool isDescriptionFixture = currentFixture != null && currentFixture.HasClass && currentFixture.ClassType == typeof(DescriptionFixture);
            string lineContent = line.Content;

            if (isImageLine)
            {
                if (currentFixture != null)
                {
                    parent.Add(currentFixture);
                    currentFixture = null;
                }
                currentFixture = new FixtureDefinition(parent);
                currentFixture.ClassName = typeof(ImageDescriptionFixture).Name;
                currentFixture.ClassType = typeof(ImageDescriptionFixture);
                lineContent = lineContent.Substring(lineContent.IndexOf(CommentToken) + CommentToken.Length + 4);
                currentFixture.WithRow(line, lineContent);
                parent.Add(currentFixture);

                return null;
            }

            if (isDescriptionLine)
            {
                lineContent = lineContent.Substring(lineContent.IndexOf(CommentToken) + CommentToken.Length);
            }

            if (currentFixture != null && isDescriptionLine != isDescriptionFixture)
            {
                parent.Add(currentFixture);
                currentFixture = null;
            }

            if (currentFixture == null)
            {
                currentFixture = new FixtureDefinition(parent);
                if (isDescriptionLine)
                {
                    currentFixture.ClassName = typeof(DescriptionFixture).Name;
                    currentFixture.ClassType = typeof(DescriptionFixture);
                }
            }

            if (isDescriptionLine)
            {
                currentFixture.WithRow(line, lineContent);
            }
            else
            {
                currentFixture.WithRow(line, 
                    line.TrimmedContent.Split(this.CellSeparator).Select(s => s.Trim()).ToArray());
            }

            return currentFixture;
        }

        private FixtureDefinition ProcessBlankLine(FixtureDefinition currentFixture, SpecificationDefinition res)
        {
            if (currentFixture != null)
            {
                res.Add(currentFixture);
                currentFixture = null;
            }
            return currentFixture;
        }
    }
}
